/*
 * @(#)PhotoFrame.java	1.18 02/10/03 @(#)
 *
 * Copyright (c) 2000-2002 Sun Microsystems, Inc.  All rights reserved.
 * PROPRIETARY/CONFIDENTIAL
 * Use is subject to license terms.
 */

package example.photoalbum;

import java.util.Vector;
import javax.microedition.lcdui.*;

/**
 * This PhotoFrame provides the picture frame and drives the animation
 * of the frames and the picture. It handles the starting and stopping
 * of the Animation and contains the Thread used to do
 * the timing and requests that the Canvas be repainted
 * periodically.
 * It controls the border style and animation speed.
 */
class PhotoFrame extends Canvas implements Runnable {

    /** border style */
    private int style;
    /** animation speed set */
    private int speed;

    /** Vector of images to display */
    private Vector images;
    /** Index of next image to display */
    private int index;
    /** X offset of image in frame */
    private int imageX;
    /** X offset of image in frame */
    private int imageY;
    /** Width and height of image */
    private int imageWidth, imageHeight;
    
    /** Thread used for triggering repaints */
    private Thread thread;
    /** buffer image of the screen */
    private Image image;
    /** Pattern image used for border */
    private Image bimage;
    /** Time of most recent paint */
    private long paintTime; 
    /** Time of most recent frame rate report */
    private long statsTime;
    /** Number of frames since last frame rate report */
    int frameCount;
    /** Last reported frame rate (for re-paint) */
    int frameRate;

    /**
     * Create a new PhotoFrame.
     * Create an offscreen mutable image into which the border is drawn.
     * Border style is none (0).
     * Speed is stopped (0) until set.
     */
    PhotoFrame() {
        image = Image.createImage(getWidth(), getHeight());
        setStyle(0);
        setSpeed(0);
    }
    
    /**
     * Set the array of images to be displayed.
     * Update the width and height of the image and draw
     * the border to fit around it in the offscreen image.
     * @param images a vector of images to be displayed.
     */
    void setImages(Vector images) {
        this.images = images;
        if (images.size() > 0) {
            Image image = (Image)images.elementAt(0);
            imageWidth = image.getWidth();
            imageHeight = image.getHeight();
        } else {
            imageWidth = 0;
            imageHeight = 0;
        }
	index = 0;
        imageX = (getWidth() - imageWidth) / 2;
        imageY = (getHeight() - imageHeight) / 2;
        genFrame(style, imageX, imageY, imageWidth, imageHeight);
    }

    /**
     * Advance to the next image and wrap around if necessary.
     */
    void next() {
        if (images == null || ++index >= images.size()) {
            index = 0;
        } 
    }

    /**
     * Back up to the previous image.
     * Wrap around to the end if at the beginning.
     */
    void previous() {
        if (images != null && --index < 0) {
            index = images.size()-1;
        } else {
	    index = 0;
	}
    }
 
    /**
     * Reset the PhotoFrame so it holds minimal resources.
     * The animation thread is stopped.
     */
    void reset() {
        images = null;
        thread = null;
    }

    /**
     * Handle key events. FIRE events toggle between
     * running and stopped.  LEFT and RIGHT key events
     * when stopped show the previous or next image.
     * @param keyCode of the key pressed
     */
    protected void keyPressed(int keyCode) {
        int action = getGameAction(keyCode);
        switch (action) {
        case RIGHT:
	    if (thread == null) {
		next();
		repaint();
	    }
            break;
        case LEFT:
	    if (thread == null) {
		previous();
		repaint();
	    }
            break;
        case FIRE:
            // Use FIRE to toggle the activity of the thread
	    if (thread == null) {
		thread = new Thread(this);
		thread.start();
	    } else {
		synchronized (this) {
		    // Wake up the thread to change the timing
		    this.notify();
		    thread = null;
		}
	    }
	    break;
        }
    }
    
    /**
     * Handle key repeat events as regular key events.
     * @param keyCode of the key repeated
     */
    protected void keyRepeated(int keyCode) {
        keyPressed(keyCode);
    }

    /**
     * Set the animation speed.
     * Speed:
     * <OL>
     * <LI>0 = stop
     * <LI>1 = slow
     * <LI>2 = medium
     * <LI>3 = fast
     * <LI>4 = unlimited
     * </OL>
     * @param speed speedo of animation 0-3;
     */
    void setSpeed(int speed) {
	this.speed = speed;
	statsTime = 0;
    }

    /**
     * Get the speed at which animation occurs.
     * @return the current speed.
     * @see setSpeed
     */
    int getSpeed() {
        return speed;
    }

    /**
     * Set the frame style.
     * Recreate the photo frame image from the current style
     * and location and size
     * <p>
     * Style:
     * <OL>
     * <LI> Style 0: No border is drawn.
     * <LI> Style 1: A simple border is drawn
     * <LI> Style 2: The border is outlined and an image
     * is created to tile within the border.
     * </OL>
     * @param style the style of the border; 0 = none, 1 = simple,
     * 2 = fancy.
     */
    void setStyle(int style) {
	this.style = style;
        genFrame(style, imageX, imageY, imageWidth, imageHeight);
    }

    /**
     * Get the style being used for borders.
     * @return the style.
     */
    int getStyle() {
        return style;
    }

    /**
     * Notified when Canvas is made visible.
     * If there is more than one image to display
     * create the thread to run the animation timing.
     */
    protected void showNotify() {
        if (images != null && images.size() > 1) {
            thread = new Thread(this);
            thread.start();
        }
    }

    /**
     * Notified when the Canvas is no longer visible.
     * Signal the running Thread that it should stop.
     */
    protected void hideNotify() {
        thread = null;
    }

    
    /**
     * Return true if the specified rectangle does not intersect
     * the clipping rectangle of the graphics object.  If it returns
     * true then the object must be drawn otherwise it would be clipped
     * completely.
     * The checks are done
     * @param g the Graphics context to check.
     * @param x the x value of the upper left corner of the rectange
     * @param y the y value of the upper left corner of the rectange
     * @param w the width of the rectangle
     * @param h the height of the rectangle
     * @return true if the rectangle intersects the clipping region
     */
    boolean intersectsClip(Graphics g, int x, int y, int w, int h) {
        int cx = g.getClipX();
        if (x + w <= cx)
            return false;

        int cw = g.getClipWidth();
        if (x > cx + cw)
            return false;

        int cy = g.getClipY();
        if (y + h <= cy)
            return false;

        int ch = g.getClipHeight();
        if (y > cy + ch)
            return false;
        return true;
    }

    /**
     * Runs the animation and makes the repaint requests.
     * The thread will exit when it is no longer the current
     * Animation thread.
     */
    public void run() {
        Thread mythread = Thread.currentThread();
        long scheduled = System.currentTimeMillis();
	statsTime = scheduled;
        paintTime = scheduled;
	frameCount = 0;
	frameRate = 0;

        while (thread == mythread) {
            synchronized (this) {
                try {
                    // Update when the next frame should be drawn
                    // and compute the delta till then
                    scheduled += speeds[speed];
                    long delta = scheduled - paintTime;
                    if (delta > 0)  {
                        this.wait(delta);
                    }
                    // Advance and repaint the screen
                    next();
		    repaint();
                    serviceRepaints();
                } catch (InterruptedException e) {
                }
            }
        }
    }

    /** 
     * Paint is called whenever the canvas should be redrawn.
     * It clears the canvas and draws the frame and the current 
     * current frame from the animation.
     * @param g the Graphics context to which to draw
     */
    protected void paint(Graphics g) {
	paintTime = System.currentTimeMillis();
	if (image != null) {
	    
	    // Draw the frame unless only the picture is being re-drawn
	    // This is the inverse of the usual clip check.
	    int cx = 0, cy = 0, cw = 0, ch = 0;
	    if ((cx = g.getClipX()) < imageX || 
		(cy = g.getClipY()) < imageY ||
		(cx + (cw = g.getClipWidth())) > (imageX + imageWidth) ||
		(cy + (ch = g.getClipHeight())) > (imageY + imageHeight)) {
		g.drawImage(image, 0, 0, Graphics.LEFT|Graphics.TOP);
		if (frameRate > 0) {
		    g.fillRect(0, getHeight(), 60, 20);
		    g.drawString("FPS = " + frameRate, 0, getHeight(), 
				 Graphics.BOTTOM|Graphics.LEFT);
		}
	    }

	    // Draw the image if it intersects the clipping region
	    if (images != null &&
		index < images.size() &&
		intersectsClip(g, imageX, imageY, imageWidth, imageHeight)) {
		g.drawImage((Image)images.elementAt(index),
			    imageX, imageY, Graphics.LEFT|Graphics.TOP);
	    }
	    frameCount++;


	    // Update Frame rate
	    int delta = (int)(paintTime - statsTime);
	    if (delta > 1000 && delta < 10000) {
		frameRate = ((frameCount * 1000 + 500) / delta);
		frameCount = 0;
		statsTime = paintTime;
		repaint();	// queue full repaint to display frame rate
	    }
	}
    }
    
    /**
     * Paint the photo frame into the buffered screen image.
     * This will avoid drawing each of its parts on each repaint.
     * Paint will only need to put the image into the frame.
     * @param style the style of frame to draw.
     * @param x the x offset of the image.
     * @param y the y offset of the image
     * @param width the width of the anmiation image
     * @param height the height of the animation image
     */
    private void genFrame(int style, int x, int y, int width, int height) {
        Graphics g = image.getGraphics();

        // Clear the entire image to white
        g.setColor(0xffffff);
        g.fillRect(0, 0, image.getWidth()+1, image.getHeight()+1);
        
        // Set the origin of the image and paint the border and image.
        g.translate(x, y);
        paintBorder(g, style, width, height);
    }

    /**
     * Draw a border of the selected style.
     * @param g graphics context to which to draw.
     * @param style of the border to display
     * @param w the width reserved for the image
     * @param h the height reserved of the image
     * @see setStyle
     */
    private void paintBorder(Graphics g, int style, int w, int h) {
	if (style == 1) {
	    g.setGrayScale(128);
	    g.drawRect(-1, -1, w + 1, h + 1);
	    g.drawRect(-2, -2, w + 3, h + 3);
	}

	if (style == 2) {
            // Draw fancy border with image between outer and inner rectangles
            if (bimage == null) 
                bimage = genBorder();          // Generate the border image
	    int bw = bimage.getWidth();
	    int bh = bimage.getHeight();
	    int i;
            // Draw the inner and outer solid border
	    g.setGrayScale(128);
	    g.drawRect(-1, -1, w + 1, h + 1);
	    g.drawRect(-bw - 2, -bh - 2, w + bw * 2 + 3, h + bh * 2 + 3);

            // Draw it in each corner
            g.drawImage(bimage, -1, -1, Graphics.BOTTOM|Graphics.RIGHT);
 	    g.drawImage(bimage, -1, h + 1, Graphics.TOP|Graphics.RIGHT);
	    g.drawImage(bimage, w + 1, -1, Graphics.BOTTOM|Graphics.LEFT);
	    g.drawImage(bimage, w + 1, h + 1, Graphics.TOP|Graphics.LEFT);

            // Draw the embedded image down left and right sides
            for (i = ((h % bh) / 2); i < h - bh; i += bh) {
		g.drawImage(bimage, -1, i, Graphics.RIGHT|Graphics.TOP);
		g.drawImage(bimage, w + 1, i, Graphics.LEFT|Graphics.TOP);
	    }
            // Draw the embedded image across the top and bottom
	    for (i = ((w % bw) / 2); i < w - bw; i += bw) {
		g.drawImage(bimage, i, -1, Graphics.LEFT|Graphics.BOTTOM);
		g.drawImage(bimage, i, h + 1, Graphics.LEFT|Graphics.TOP);
	    }
	}
    }

    /**
     * Create an image for the border.
     * The border consists of a simple "+" drawn in a 5x5 image.
     * Fill the image with white and draw the "+" as magenta.
     * @return the image initialized with the pattern
     */
    private Image genBorder() {
        Image image = Image.createImage(5, 5);
        Graphics g = image.getGraphics();
        g.setColor(255, 255, 255);
        g.fillRect(0, 0, 5, 5);
        g.setColor(128, 0, 255);
        g.drawLine(2, 1, 2, 3); // vertical
        g.drawLine(1, 2, 3, 2); // horizontal
        return image;
    }

    /**
     * Mapping of speed values to delays in ms.
     * Indices map to those in the speed ChoiceGroup in the options form.
     * The indices are: 0 = stop, 1 = slow, 2 = medium, 3 = fast.
     * @see setSpeed
     */
    private final static int speeds[] = {999999999, 500, 250, 100, 0};
}
